'''
    Random_Picker 用Python实现的随机点名器
    Copyright (C) 2022 海Cha

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
'''

import xlrd as excel
from tkinter import *
from tkinter.ttk import *
import tkinter.messagebox as msgbox
import random
import sys
import winsound
import webbrowser
import time
import requests
import os
import base64

version = "v1.2.3"

filename = "./data/names.xls"
configname = "./data/config.data"
selname = "./assets/sel.wav"
procname = "./assets/proc.wav"
iconname = "./assets/点名器.ico"
gui_size = "550x125"

appname = '点名器'
author = "海Cha"
copyright = appname + ' ' + version + ' Copyright (C) 2022 ' + author

protection_override = False
up_percent = 30
down_persent = 40
elim_rows = 1
percent_override = True

elim_id = []
up_id = []
down_id = []

sound = True

students = []
classes = []
subjects = []
filter = []

last_choice = ""

gui = Tk()

gui.withdraw()
gui.title(appname)
gui.geometry(gui_size)
gui.resizable(0,0)
gui.configure(bg = "white")
topmost = False
if_exit = False

logging = True
log_level = "INFO"
log_file = ""

def Decode(str):
    decode = base64.b64decode(str).decode("utf-8")
    logs("DEBUG","获得base64解码:" + decode)
    return decode

def newlog(type = "INFO"):
    global log_file
    if logging:
        if not os.path.exists(".\\log\\"):
            os.makedirs(".\\log\\")
        curtime = time.strftime('%Y-%m-%d [%H-%M-%S]', time.localtime())
        log_file = ".\\log\\" + curtime + ".log"
        with open(log_file,mode = "w",encoding="utf-8") as log:
            log.write("点名器日志文件 " + log_file + "\n")
            log.write(copyright + '\n')
            log.write("若点名器软件出现预期外行为或崩溃，请将此文件发送给作者。\n")
            log.write("若需发布该日志文件到公开网络平台，请注意屏蔽日志文件中涉及的计算机敏感信息。\n")
            log.write("当前日志等级：" + type + "\n")
            log.write("若需改变日志记录等级，请在启动主程序时传入 --log-level-**INFO/WARN/ERROR/FATAL/SILENT**\n")
            log.write("若需关闭日志记录，请在启动主程序时传入 --disable-logging。\n")

def logtype_to_int(type):
    if type == "DEBUG":
        return 0
    elif type == "INFO":
        return 1
    elif type == "WARN":
        return 2
    elif type == "ERROR":
        return 3
    elif type == "FATAL":
        return 4
    elif type == "SILENT":
        return 5
    elif type == "SYSTEM":
        return 6

def logs(type, message):
    if logging:
        curtime = time.strftime('%Y-%m-%d [%H-%M-%S]', time.localtime())
        logline = ""
        if logtype_to_int(type) >= logtype_to_int(log_level):
            logline = "\n[" + type + "] " + curtime + " " + message
        with open(log_file,mode = 'a',encoding="utf-8") as log:
            log.write(logline)

try:
    if len(sys.argv) > 1:
        for arg in sys.argv[1:]:
            if arg == "--licence":
                if msgbox.askokcancel('许可证信息', copyright + '\n本程序是开源程序，受开源协议 GNU General Public Licence v3.0 (GPL v3) 保护\n基于GPL v3协议，本程序没有任何质量保证。\n这是一个自由软件，欢迎再次分发。\n点击「确认」将会由系统默认浏览器打开LICENCE文件\n出现此对话框的原因是由于您在运行本软件时传入了 --licence 参数，欲启动主程序，请移除该参数。'):
                    webbrowser.open_new_tab('https://www.gnu.org/licenses/gpl-3.0.html')
                if_exit = True
            elif arg == "--version":
                msgbox.showinfo('版本信息', copyright + '\n出现此对话框的原因是由于您在运行本软件时传入了 --version 参数，欲启动主程序，请移除该参数。')
                if_exit = True
            elif arg == "--help":
                msgbox.showinfo('帮助信息','----- 点名器 ' + version + ' -----\n--licence 显示许可证信息\n--version 显示版本信息\n--path 显示程序运行路径\n--help 显示此信息\n欲打开主程序GUI，则无需传参\n出现此对话框的原因是由于您在运行本软件时传入了 --help 参数，欲启动主程序，请移除该参数。')
                if_exit = True
            elif arg == "--path":
                msgbox.showinfo('路径信息',copyright + '\n当前程序运行在 ' + sys.argv[0] + ' 路径上\n出现此对话框的原因是由于您在运行本软件时传入了 --path 参数，欲启动主程序，请移除该参数。')
                if_exit = True
            elif arg == "--top-most":
                msgbox.showinfo('设置成功',copyright + '\n点名器将会置顶运行。\n出现此对话框的原因是由于您在运行本软件时传入了 --top-most 参数，欲关闭置顶运行，请移除该参数。')
            elif arg == "--disable-logging":
                msgbox.showinfo('设置成功',copyright + '\n调试日志已关闭。程序出现预期外动作或崩溃时，请移除此参数以获得日志。\n出现此对话框的原因是由于您在运行本软件时传入了 --disable-logging 参数，欲重启日志记录，请移除该参数。')
                logging = False
            elif arg == "--log-level-DEBUG":
                msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 DEBUG\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。')
                log_level = "DEBUG"
            elif arg == "--log-level-INFO":
                msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 INFO\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。')
                log_level = "INFO"
            elif arg == "--log-level-WARN":
                msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 WARN\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。')
                log_level = "WARN"
            elif arg == "--log-level-ERROR":
                msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 ERROR\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。')
                log_level = "ERROR"
            elif arg == "--log-level-FATAL":
                msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 FATAL\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。')
                log_level = "FATAL"
            elif arg == "--log-level-SILENT":
                msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 SILENT\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。')
                log_level = "SILENT"
            elif arg.find("--") != -1:
                msgbox.showerror('传参错误','无法识别参数 ' + arg + ' 请调整您的启动参数\n传入 --help以查看帮助信息\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数，欲启动主程序，请移除该参数。')
                if_exit = True
except:
    if_exit = False
finally:
    if if_exit:
        sys.exit(0)

newlog(log_level)

try:
    if logtype_to_int(log_level) <= logtype_to_int("DEBUG"):
        try:
            ip = requests.get('http://myip.ipip.net',timeout=5).text.replace('\n', '').replace('\r', '')
        except:
            ip = "当前IP：无公网IP"

        logs("SYSTEM","日志记录已激活")
        logs("DEBUG","-----系统基本信息-----")
        logs("DEBUG",ip)
        logs("DEBUG","运行平台：" + sys.platform)
        logs("DEBUG","当前运行目录：" + sys.argv[0] + '\n')

    try:
        logs("DEBUG","加载图片文件 " + iconname)
        file = open(iconname)
        file.close()
        gui.iconbitmap(iconname)
    except IOError:
        logs("ERROR","资源加载错误:在 ./assets目录下未能找到 点名器.ico 文件")
        if not msgbox.askokcancel('资源文件错误', '在 ./assets目录下未能找到 点名器.ico 文件,点击「确定」将加载默认图标,点击「取消」以关闭程序'):
            sys.exit()
        logs("WARN","图标加载错误，回滚到默认图标")

    try:
        logs("DEBUG","加载音频资源文件 " + selname)
        file = open(selname)
        file.close()
        file = open(procname)
        logs("DEBUG","加载音频资源文件 " + procname)
        file.close()
    except IOError:
        logs("ERROR","资源加载错误:在 ./assets目录下未能找到音频文件")
        if msgbox.askokcancel('资源文件错误', '在 ./assets目录下未能找到音频文件,点击「确定」将关闭声音,点击「取消」以关闭程序'):
            logs("WARN","声音已关闭")
            sound = False
        else:
            sys.exit()

    class Student(object):
        name = ""
        subject = ""
        id = ""
        class_name = ""

        def __init__(self,id,class_name,subject,name):
            self.id = id
            self.class_name = class_name
            self.subject = subject
            self.name = name

    try:
        logs("DEBUG","加载数据库文件 " + filename)
        with excel.open_workbook(filename) as excel_data:
            table = excel_data.sheets()[0]
            # win32api.SetFileAttributes(filename,win32con.FILE_ATTRIBUTE_HIDDEN)
    except FileNotFoundError:
        logs("FATAL","数据库加载错误:在 ./data下未能找到 names.xls 文件")
        msgbox.showerror('找不到数据库文件', '在 ./data下未能找到 names.xls 文件,请确认您的文件存在')
        sys.exit()

    try:
        cols = table.ncols
        rows = table.nrows
        logs("DEBUG","读取数据库行数:" + str(cols) + " 列数:" + str(rows))
        assert cols == 4
        assert rows > 1
    except AssertionError:
        logs("FATAL","数据库加载错误:数据库文件内行/列数不合法，当前行数:" + str(rows) + ' 当前列数:' + str(cols))
        msgbox.showerror('数据库文件格式不正确', '数据库文件内行/列数不合法,请确认数据库文件拥有超过1的行数和等于4的列数,当前行数:' + str(rows) + ' 当前列数:' + str(cols))
        sys.exit()

    try:
        logs("DEBUG","加载概率修正配置文件 " + configname)
        with open(configname,'r') as file:
            config = file.read().splitlines()
            for unit in config:
                logs("DEBUG","读取到概率修正密文：" + unit)
                unit = Decode(unit)
                temp = unit.split(",")
                assert temp[1] == '0' or temp[1] == '+' or temp[1] == '-'
                if temp[1] == '0':
                    logs("DEBUG","应用概率修正:移除ID " + temp[0])
                    elim_id.append(int(temp[0]))
                elif temp[1] == '+':
                    logs("DEBUG","应用概率修正:标记ID为提升 " + temp[0])
                    up_id.append(int(temp[0]))
                elif temp[1] == "-":
                    logs("DEBUG","应用概率修正:标记ID为降低 " + temp[0])
                    down_id.append(int(temp[0]))
                else:
                    pass
    except Exception as e:
        logs("DEBUG","加载概率修正出现错误:" + str(e))
        percent_override = False

    up_percent = len(up_id) * 5 if len(up_id) <= 6 else 30
    down_percent = len(down_id) * 8 if len(down_id) <= 5 else 40
    logs("DEBUG","应用概率修正组概率：概率提升组:" + str(up_percent) + "% 概率降低组:" + str(down_percent) + "%")

    gui.deiconify()

    logs("DEBUG","数据库内容写入内存")
    for i in range(elim_rows,rows):
        tmp_list = [str(table.cell_value(i,j)) for j in range(0,cols)]
        if (int(float(tmp_list[0])) not in elim_id):
            student = Student(tmp_list[0],tmp_list[1],tmp_list[2],tmp_list[3])
            logs("DEBUG","更新数据库内容到内存：" + str(student.id))
            students.append(student)

    logs("DEBUG","创建数据库备份cur_stu")
    cur_stu = students[:]

    logs("DEBUG","创建班级列表")
    for student in students:
        if student.class_name not in classes:
            classes.append(student.class_name)
        if student.subject not in subjects:
            subjects.append(student.subject)

    cur_name = StringVar()

    def upd_name(name):
        logs("DEBUG","更新cur_name显示：" + name)
        cur_name.set(name)

    upd_name("")

    listbox = Listbox(gui, selectmode = MULTIPLE, height = 5)

    for tmp_class in classes:
        listbox.insert("end",tmp_class)

    def sel_class(flag = 1):
        global filter, listbox, classes, cur_stu, students, percent_override
        filter.clear()
        for selection in listbox.curselection():
            # print(selection)
            logs("DEBUG","添加班级筛选：" + classes[selection])
            filter.append(classes[selection])
        logs("DEBUG","班级筛选名单：" + str(filter))
        # print(filter)
        if filter:
            logs("INFO","已应用班级选择")
            if flag:
                msgbox.showinfo('班级选择已应用', '已应用当前班级选择', parent=gui)
                logs("DEBUG","概率修正已禁用")
                percent_override = False
            cur_stu.clear()
            for student in students:
                if student.class_name in filter:
                    # print(student.name)
                    cur_stu.append(student)
        else:
            logs("INFO","未选择任何班级，默认选择所有班级")
            if flag:
                msgbox.showwarning('班级选择已应用', '当前设置会选择所有班级的名单,这样对吗?', parent=gui)
                logs("DEBUG","概率修正已禁用")
                percent_override = False
            filter = classes[:]
            cur_stu = students[:]

    def choose():
        global last_choice, cur_stu
        logs("INFO","开始新一轮随机挑选")
        logs("DEBUG","上一轮的选择：" + str(last_choice))
        logs("DEBUG","重新应用班级筛选(静默)")
        sel_class(0)
        # 触发30%概率,从up中挑选
        if (random.randint(1,100) < up_percent and percent_override):
            logs("DEBUG","从概率提升组中挑选")
            logs("DEBUG","防重选保护关闭")
            protection_override = True;
            temp = []
            for student in cur_stu:
                if int(float(student.id)) in up_id:
                    logs("DEBUG","学生id:" + str(student.id) + "学生姓名:" + str(student.name) + "加入至当前挑选名单")
                    temp.append(student)
            logs("DEBUG","挑选名单重筛完毕")
            cur_stu = temp[:]
        elif (random.randint(1,100) < down_persent and percent_override):
            logs("DEBUG","概率降低组人员排除")
            logs("DEBUG","防重选保护关闭")
            protection_override = True;
            temp = [];
            for student in cur_stu:
                if int(float(student.id)) not in down_id:
                    logs("DEBUG","学生id:" + str(student.id) + "学生姓名:" + str(student.name) + "加入至当前挑选名单")
                    temp.append(student)
            logs("DEBUG","挑选名单重筛完毕")
            cur_stu = temp[:]
        else:
            protection_override = False
        if not cur_stu:
            protection_override = False
            sel_class(0)
        logs("DEBUG","开始随机挑选")
        choice = random.choice(cur_stu)
        logs("DEBUG","随机选中人员 id:" + choice.id + " 姓名:" + choice.name + " 学科:" + choice.subject)
        # print(choice.name)
        counter = 0
        while (last_choice == choice.name and (not protection_override) and (len(cur_stu) > 1)) or choice.id in elim_id:
            logs("DEBUG","防重选保护激活：重选人员")
            choice = random.choice(cur_stu)
            logs("DEBUG","随机选中人员 id:" + choice.id + " 姓名:" + choice.name + " 学科:" + choice.subject)
            counter = counter + 1
            if counter >= 100:
                logs("DEBUG","防重选保护关闭：重选次数过多")
                break
        logs("DEBUG","重选保护重筛完毕,应用选择人员")
        logs("SILENT","选中人员 id:" + choice.id + " 姓名:" + choice.name + " 学科:" + choice.subject)
        upd_name(choice.name)
        logs("DEBUG","更新班级Label显示：" + choice.class_name)
        class_label.config(text = choice.class_name)
        logs("DEBUG","更新学科Label显示：" + choice.subject)
        subject_label.config(text = choice.subject)
        logs("DEBUG","更新防重选保护人员记录：" + choice.name)
        last_choice = choice.name
        if sound:
            logs("DEBUG","播放 " + selname + " 选中音频文件")
            winsound.PlaySound(selname, winsound.SND_ASYNC | winsound.SND_FILENAME)

    logs("DEBUG","设置GUI控件样式")
    startramdonStyle=Style()
    startramdonStyle.configure("SR.TButton", font = ("宋体", 17, "bold"), foreground = "red", background = "white",width=29)
    setclassStyle=Style()
    setclassStyle.configure("SC.TButton", font = ("宋体", 17), background = "white",height="20",width=13)
    namelabelStyle=Style()
    namelabelStyle.configure("NL.TLabel", font = ("宋体", 30, "bold"), background = "white", foreground = "blue",width=15,anchor="center")
    classlabelStyle=Style()
    classlabelStyle.configure("CL.TLabel", font = ("宋体", 15), background = "white",width=20,anchor="center")
    subjectlabelStyle=Style()
    subjectlabelStyle.configure("SL.TLabel", font = ("宋体", 15), background = "white",width=20,anchor="center")

    logs("DEBUG","设置控件变量连接")
    startrandom = Button(gui, text = "立刻摇人!", command = choose, style="SR.TButton")
    setclass = Button(gui, text = "应用班级选用", command = sel_class, style="SC.TButton")
    scrool = Scrollbar(gui, command = listbox.yview)
    listbox.config(yscrollcommand=scrool.set)

    name_label = Label(gui, textvariable = cur_name, style="NL.TLabel")
    class_label = Label(gui, text = "", style="CL.TLabel")
    subject_label = Label(gui, text = "", style="SL.TLabel")

    # class_label.grid(row = 2, column = 1, sticky = N+S+E+W)
    # subject_label.grid(row = 2, column = 2, sticky = N+S+E+W)
    # name_label.grid(row = 1, column = 1, sticky = N+S)
    # listbox.grid(row = 1, column = 2, sticky = N+S)
    # startrandom.grid(row = 3, column = 1, sticky = N+S+E+W)
    # setclass.grid(row = 3, column = 2, sticky = N+S+E+W)
    # scrool.grid(row = 1, column = 3, sticky = N+S)

    logs("DEBUG","绘制GUI")
    class_label.place(relx=0.3,rely=0.5)
    subject_label.place(relx=0,rely=0.5)
    name_label.place(relx=0.05,rely=0.05)
    listbox.place(relx=0.7,rely=0)
    startrandom.place(relx=0,rely=0.7375)
    setclass.place(relx=0.7,rely=0.7375)
    scrool.place(relx=0.965,rely=0)

    if topmost:
        logs("INFO","应用在置顶模式下运行")
        gui.wm_attributes("-topmost", 1)

    gui.mainloop()
except Exception as e:
    logs("FATAL","运行发生了错误：" + str(e))